package com.smartandroid.sa.tag.select;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.smartandroid.sa.tag.helper.Validate;
import com.smartandroid.sa.tag.nodes.Comment;
import com.smartandroid.sa.tag.nodes.Document;
import com.smartandroid.sa.tag.nodes.DocumentType;
import com.smartandroid.sa.tag.nodes.Element;
import com.smartandroid.sa.tag.nodes.Node;
import com.smartandroid.sa.tag.nodes.XmlDeclaration;

/**
 * Evaluates that an element matches the selector.
 */
public abstract class Evaluator {
	protected Evaluator() {
	}

	/**
	 * Test if the element meets the evaluator's requirements.
	 * 
	 * @param root
	 *            Root of the matching subtree
	 * @param element
	 *            tested element
	 */
	public abstract boolean matches(Element root, Element element);

	/**
	 * Evaluator for tag name
	 */
	public static final class Tag extends Evaluator {
		private String tagName;

		public Tag(String tagName) {
			this.tagName = tagName;
		}

		@Override
		public boolean matches(Element root, Element element) {
			return (element.tagName().equals(tagName));
		}

		@Override
		public String toString() {
			return String.format("%s", tagName);
		}
	}

	/**
	 * Evaluator for element id
	 */
	public static final class Id extends Evaluator {
		private String id;

		public Id(String id) {
			this.id = id;
		}

		@Override
		public boolean matches(Element root, Element element) {
			return (id.equals(element.id()));
		}

		@Override
		public String toString() {
			return String.format("#%s", id);
		}

	}

	/**
	 * Evaluator for element class
	 */
	public static final class Class extends Evaluator {
		private String className;

		public Class(String className) {
			this.className = className;
		}

		@Override
		public boolean matches(Element root, Element element) {
			return (element.hasClass(className));
		}

		@Override
		public String toString() {
			return String.format(".%s", className);
		}

	}

	/**
	 * Evaluator for attribute name matching
	 */
	public static final class Attribute extends Evaluator {
		private String key;

		public Attribute(String key) {
			this.key = key;
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.hasAttr(key);
		}

		@Override
		public String toString() {
			return String.format("[%s]", key);
		}

	}

	/**
	 * Evaluator for attribute name prefix matching
	 */
	public static final class AttributeStarting extends Evaluator {
		private String keyPrefix;

		public AttributeStarting(String keyPrefix) {
			this.keyPrefix = keyPrefix;
		}

		@Override
		public boolean matches(Element root, Element element) {
			List<com.smartandroid.sa.tag.nodes.Attribute> values = element
					.attributes().asList();
			for (com.smartandroid.sa.tag.nodes.Attribute attribute : values) {
				if (attribute.getKey().startsWith(keyPrefix))
					return true;
			}
			return false;
		}

		@Override
		public String toString() {
			return String.format("[^%s]", keyPrefix);
		}

	}

	/**
	 * Evaluator for attribute name/value matching
	 */
	public static final class AttributeWithValue extends AttributeKeyPair {
		public AttributeWithValue(String key, String value) {
			super(key, value);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.hasAttr(key)
					&& value.equalsIgnoreCase(element.attr(key));
		}

		@Override
		public String toString() {
			return String.format("[%s=%s]", key, value);
		}

	}

	/**
	 * Evaluator for attribute name != value matching
	 */
	public static final class AttributeWithValueNot extends AttributeKeyPair {
		public AttributeWithValueNot(String key, String value) {
			super(key, value);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return !value.equalsIgnoreCase(element.attr(key));
		}

		@Override
		public String toString() {
			return String.format("[%s!=%s]", key, value);
		}

	}

	/**
	 * Evaluator for attribute name/value matching (value prefix)
	 */
	public static final class AttributeWithValueStarting extends
			AttributeKeyPair {
		public AttributeWithValueStarting(String key, String value) {
			super(key, value);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.hasAttr(key)
					&& element.attr(key).toLowerCase().startsWith(value); // value
																			// is
																			// lower
																			// case
																			// already
		}

		@Override
		public String toString() {
			return String.format("[%s^=%s]", key, value);
		}

	}

	/**
	 * Evaluator for attribute name/value matching (value ending)
	 */
	public static final class AttributeWithValueEnding extends AttributeKeyPair {
		public AttributeWithValueEnding(String key, String value) {
			super(key, value);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.hasAttr(key)
					&& element.attr(key).toLowerCase().endsWith(value); // value
																		// is
																		// lower
																		// case
		}

		@Override
		public String toString() {
			return String.format("[%s$=%s]", key, value);
		}

	}

	/**
	 * Evaluator for attribute name/value matching (value containing)
	 */
	public static final class AttributeWithValueContaining extends
			AttributeKeyPair {
		public AttributeWithValueContaining(String key, String value) {
			super(key, value);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.hasAttr(key)
					&& element.attr(key).toLowerCase().contains(value); // value
																		// is
																		// lower
																		// case
		}

		@Override
		public String toString() {
			return String.format("[%s*=%s]", key, value);
		}

	}

	/**
	 * Evaluator for attribute name/value matching (value regex matching)
	 */
	public static final class AttributeWithValueMatching extends Evaluator {
		String key;
		Pattern pattern;

		public AttributeWithValueMatching(String key, Pattern pattern) {
			this.key = key.trim().toLowerCase();
			this.pattern = pattern;
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.hasAttr(key)
					&& pattern.matcher(element.attr(key)).find();
		}

		@Override
		public String toString() {
			return String.format("[%s~=%s]", key, pattern.toString());
		}

	}

	/**
	 * Abstract evaluator for attribute name/value matching
	 */
	public abstract static class AttributeKeyPair extends Evaluator {
		String key;
		String value;

		public AttributeKeyPair(String key, String value) {
			Validate.notEmpty(key);
			Validate.notEmpty(value);

			this.key = key.trim().toLowerCase();
			this.value = value.trim().toLowerCase();
		}
	}

	/**
	 * Evaluator for any / all element matching
	 */
	public static final class AllElements extends Evaluator {

		@Override
		public boolean matches(Element root, Element element) {
			return true;
		}

		@Override
		public String toString() {
			return "*";
		}
	}

	/**
	 * Evaluator for matching by sibling index number (e < idx)
	 */
	public static final class IndexLessThan extends IndexEvaluator {
		public IndexLessThan(int index) {
			super(index);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.elementSiblingIndex() < index;
		}

		@Override
		public String toString() {
			return String.format(":lt(%d)", index);
		}

	}

	/**
	 * Evaluator for matching by sibling index number (e > idx)
	 */
	public static final class IndexGreaterThan extends IndexEvaluator {
		public IndexGreaterThan(int index) {
			super(index);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.elementSiblingIndex() > index;
		}

		@Override
		public String toString() {
			return String.format(":gt(%d)", index);
		}

	}

	/**
	 * Evaluator for matching by sibling index number (e = idx)
	 */
	public static final class IndexEquals extends IndexEvaluator {
		public IndexEquals(int index) {
			super(index);
		}

		@Override
		public boolean matches(Element root, Element element) {
			return element.elementSiblingIndex() == index;
		}

		@Override
		public String toString() {
			return String.format(":eq(%d)", index);
		}

	}

	/**
	 * Evaluator for matching the last sibling (css :last-child)
	 */
	public static final class IsLastChild extends Evaluator {
		@Override
		public boolean matches(Element root, Element element) {
			final Element p = element.parent();
			return p != null && !(p instanceof Document)
					&& element.elementSiblingIndex() == p.children().size() - 1;
		}

		@Override
		public String toString() {
			return ":last-child";
		}
	}

	public static final class IsFirstOfType extends IsNthOfType {
		public IsFirstOfType() {
			super(0, 1);
		}

		@Override
		public String toString() {
			return ":first-of-type";
		}
	}

	public static final class IsLastOfType extends IsNthLastOfType {
		public IsLastOfType() {
			super(0, 1);
		}

		@Override
		public String toString() {
			return ":last-of-type";
		}
	}

	public static abstract class CssNthEvaluator extends Evaluator {
		protected final int a, b;

		public CssNthEvaluator(int a, int b) {
			this.a = a;
			this.b = b;
		}

		public CssNthEvaluator(int b) {
			this(0, b);
		}

		@Override
		public boolean matches(Element root, Element element) {
			final Element p = element.parent();
			if (p == null || (p instanceof Document))
				return false;

			final int pos = calculatePosition(root, element);
			if (a == 0)
				return pos == b;

			return (pos - b) * a >= 0 && (pos - b) % a == 0;
		}

		@Override
		public String toString() {
			if (a == 0)
				return String.format(":%s(%d)", getPseudoClass(), b);
			if (b == 0)
				return String.format(":%s(%dn)", getPseudoClass(), a);
			return String.format(":%s(%dn%+d)", getPseudoClass(), a, b);
		}

		protected abstract String getPseudoClass();

		protected abstract int calculatePosition(Element root, Element element);
	}

	/**
	 * css-compatible Evaluator for :eq (css :nth-child)
	 * 
	 * @see IndexEquals
	 */
	public static final class IsNthChild extends CssNthEvaluator {

		public IsNthChild(int a, int b) {
			super(a, b);
		}

		protected int calculatePosition(Element root, Element element) {
			return element.elementSiblingIndex() + 1;
		}

		protected String getPseudoClass() {
			return "nth-child";
		}
	}

	/**
	 * css pseudo class :nth-last-child)
	 * 
	 * @see IndexEquals
	 */
	public static final class IsNthLastChild extends CssNthEvaluator {
		public IsNthLastChild(int a, int b) {
			super(a, b);
		}

		@Override
		protected int calculatePosition(Element root, Element element) {
			return element.parent().children().size()
					- element.elementSiblingIndex();
		}

		@Override
		protected String getPseudoClass() {
			return "nth-last-child";
		}
	}

	/**
	 * css pseudo class nth-of-type
	 * 
	 */
	public static class IsNthOfType extends CssNthEvaluator {
		public IsNthOfType(int a, int b) {
			super(a, b);
		}

		protected int calculatePosition(Element root, Element element) {
			int pos = 0;
			Elements family = element.parent().children();
			for (int i = 0; i < family.size(); i++) {
				if (family.get(i).tag() == element.tag())
					pos++;
				if (family.get(i) == element)
					break;
			}
			return pos;
		}

		@Override
		protected String getPseudoClass() {
			return "nth-of-type";
		}
	}

	public static class IsNthLastOfType extends CssNthEvaluator {

		public IsNthLastOfType(int a, int b) {
			super(a, b);
		}

		@Override
		protected int calculatePosition(Element root, Element element) {
			int pos = 0;
			Elements family = element.parent().children();
			for (int i = element.elementSiblingIndex(); i < family.size(); i++) {
				if (family.get(i).tag() == element.tag())
					pos++;
			}
			return pos;
		}

		@Override
		protected String getPseudoClass() {
			return "nth-last-of-type";
		}
	}

	/**
	 * Evaluator for matching the first sibling (css :first-child)
	 */
	public static final class IsFirstChild extends Evaluator {
		@Override
		public boolean matches(Element root, Element element) {
			final Element p = element.parent();
			return p != null && !(p instanceof Document)
					&& element.elementSiblingIndex() == 0;
		}

		@Override
		public String toString() {
			return ":first-child";
		}
	}

	/**
	 * css3 pseudo-class :root
	 * 
	 * @see <a href="http://www.w3.org/TR/selectors/#root-pseudo">:root
	 *      selector</a>
	 * 
	 */
	public static final class IsRoot extends Evaluator {
		@Override
		public boolean matches(Element root, Element element) {
			final Element r = root instanceof Document ? root.child(0) : root;
			return element == r;
		}

		@Override
		public String toString() {
			return ":root";
		}
	}

	public static final class IsOnlyChild extends Evaluator {
		@Override
		public boolean matches(Element root, Element element) {
			final Element p = element.parent();
			return p != null && !(p instanceof Document)
					&& element.siblingElements().size() == 0;
		}

		@Override
		public String toString() {
			return ":only-child";
		}
	}

	public static final class IsOnlyOfType extends Evaluator {
		@Override
		public boolean matches(Element root, Element element) {
			final Element p = element.parent();
			if (p == null || p instanceof Document)
				return false;

			int pos = 0;
			Elements family = p.children();
			for (int i = 0; i < family.size(); i++) {
				if (family.get(i).tag().equals(element.tag()))
					pos++;
			}
			return pos == 1;
		}

		@Override
		public String toString() {
			return ":only-of-type";
		}
	}

	public static final class IsEmpty extends Evaluator {
		@Override
		public boolean matches(Element root, Element element) {
			List<Node> family = element.childNodes();
			for (int i = 0; i < family.size(); i++) {
				Node n = family.get(i);
				if (!(n instanceof Comment || n instanceof XmlDeclaration || n instanceof DocumentType))
					return false;
			}
			return true;
		}

		@Override
		public String toString() {
			return ":empty";
		}
	}

	/**
	 * Abstract evaluator for sibling index matching
	 * 
	 * @author ant
	 */
	public abstract static class IndexEvaluator extends Evaluator {
		int index;

		public IndexEvaluator(int index) {
			this.index = index;
		}
	}

	/**
	 * Evaluator for matching Element (and its descendants) text
	 */
	public static final class ContainsText extends Evaluator {
		private String searchText;

		public ContainsText(String searchText) {
			this.searchText = searchText.toLowerCase();
		}

		@Override
		public boolean matches(Element root, Element element) {
			return (element.text().toLowerCase().contains(searchText));
		}

		@Override
		public String toString() {
			return String.format(":contains(%s", searchText);
		}
	}

	/**
	 * Evaluator for matching Element's own text
	 */
	public static final class ContainsOwnText extends Evaluator {
		private String searchText;

		public ContainsOwnText(String searchText) {
			this.searchText = searchText.toLowerCase();
		}

		@Override
		public boolean matches(Element root, Element element) {
			return (element.ownText().toLowerCase().contains(searchText));
		}

		@Override
		public String toString() {
			return String.format(":containsOwn(%s", searchText);
		}
	}

	/**
	 * Evaluator for matching Element (and its descendants) text with regex
	 */
	public static final class Matches extends Evaluator {
		private Pattern pattern;

		public Matches(Pattern pattern) {
			this.pattern = pattern;
		}

		@Override
		public boolean matches(Element root, Element element) {
			Matcher m = pattern.matcher(element.text());
			return m.find();
		}

		@Override
		public String toString() {
			return String.format(":matches(%s", pattern);
		}
	}

	/**
	 * Evaluator for matching Element's own text with regex
	 */
	public static final class MatchesOwn extends Evaluator {
		private Pattern pattern;

		public MatchesOwn(Pattern pattern) {
			this.pattern = pattern;
		}

		@Override
		public boolean matches(Element root, Element element) {
			Matcher m = pattern.matcher(element.ownText());
			return m.find();
		}

		@Override
		public String toString() {
			return String.format(":matchesOwn(%s", pattern);
		}
	}
}
